drawing

Проект: Анализ резюме из HeadHunter

In [85]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px

Исследование структуры данных¶

  1. Прочитайте данные с помощью библиотеки Pandas. Совет: перед чтением обратите внимание на разделитель внутри файла.
In [99]:
#ваш код здесь
hh = pd.read_csv("D:/IDE/SF - Assign/hh.csv", sep= ";")
print(hh.shape)
(44744, 12)
  1. Выведите несколько первых (последних) строк таблицы, чтобы убедиться в том, что ваши данные не повреждены. Ознакомьтесь с признаками и их структурой.
In [102]:
#ваш код здесь
display(hh.head(10))
Пол, возраст ЗП Ищет работу на должность: Город, переезд, командировки Занятость График Опыт работы Последнее/нынешнее место работы Последняя/нынешняя должность Образование и ВУЗ Обновление резюме Авто
0 Мужчина , 39 лет , родился 27 ноября 1979 29000 руб. Системный администратор Советск (Калининградская область) , не готов к... частичная занятость, проектная работа, полная ... гибкий график, полный день, сменный график, ва... Опыт работы 16 лет 10 месяцев Август 2010 — п... МАОУ "СОШ № 1 г.Немана" Системный администратор Неоконченное высшее образование 2000 Балтийск... 16.04.2019 15:59 Имеется собственный автомобиль
1 Мужчина , 60 лет , родился 20 марта 1959 40000 руб. Технический писатель Королев , не готов к переезду , готов к редким... частичная занятость, проектная работа, полная ... гибкий график, полный день, сменный график, уд... Опыт работы 19 лет 5 месяцев Январь 2000 — по... Временный трудовой коллектив Менеджер проекта, Аналитик, Технический писатель Высшее образование 1981 Военно-космическая ак... 12.04.2019 08:42 Не указано
2 Женщина , 36 лет , родилась 12 августа 1982 20000 руб. Оператор Тверь , не готова к переезду , не готова к ком... полная занятость полный день Опыт работы 10 лет 3 месяца Октябрь 2004 — Де... ПАО Сбербанк Кассир-операционист Среднее специальное образование 2002 Профессио... 16.04.2019 08:35 Не указано
3 Мужчина , 38 лет , родился 25 июня 1980 100000 руб. Веб-разработчик (HTML / CSS / JS / PHP / базы ... Саратов , не готов к переезду , готов к редким... частичная занятость, проектная работа, полная ... гибкий график, удаленная работа Опыт работы 18 лет 9 месяцев Август 2017 — Ап... OpenSoft Инженер-программист Высшее образование 2002 Саратовский государст... 08.04.2019 14:23 Не указано
4 Женщина , 26 лет , родилась 3 марта 1993 140000 руб. Региональный менеджер по продажам Москва , не готова к переезду , готова к коман... полная занятость полный день Опыт работы 5 лет 7 месяцев Региональный мене... Мармелад Менеджер по продажам Высшее образование 2015 Кгу Психологии и педаг... 22.04.2019 10:32 Не указано
5 Мужчина , 29 лет , родился 5 октября 1989 25000 руб. Технический специалист Старый Оскол , не готов к переезду , не готов ... полная занятость полный день, сменный график Опыт работы 9 лет 9 месяцев Технический специ... Комбинат хлебопродуктов Старооскольский Слесарь КИПиА Неоконченное высшее образование 2013 Воронежс... 22.04.2019 15:59 Имеется собственный автомобиль
6 Мужчина , 46 лет , родился 19 сентября 1972 250000 руб. Руководитель ИТ-проектов Москва , не готов к переезду , готов к редким ... полная занятость полный день Опыт работы 22 года 9 месяцев Руководитель ИТ... СИБИНТЕК, ООО ИК Менеджер (Руководитель ИТ-проектов) Высшее образование 2008 ФГОУ ВПО «Уральская ак... 25.04.2019 22:48 Не указано
7 Мужчина , 29 лет , родился 9 июля 1989 70000 руб. Инженер АСУ ТП Москва , м. Бульвар Рокоссовского , готов к п... полная занятость полный день Опыт работы 3 года 11 месяцев Декабрь 2016 — ... ФМ-инжиниринг Инженер АСУ ТП Высшее образование 2014 Белорусская Государств... 07.05.2019 17:59 Не указано
8 Мужчина , 29 лет , родился 11 июля 1989 65000 руб. Ревизор Москва , м. Шоссе Энтузиастов , готов к перее... полная занятость полный день Опыт работы 8 лет 9 месяцев Декабрь 2016 — по... ФГБУ РСВО Старший специалист Неоконченное высшее образование 2020 Московск... 11.04.2019 11:08 Имеется собственный автомобиль
9 Мужчина , 34 года , родился 26 мая 1984 55000 руб. Менеджер по работе с клиентами, Pre-sale менед... Москва , м. Теплый Стан , не готов к переезду... полная занятость полный день Опыт работы 16 лет 6 месяцев Июнь 2018 — по н... ООО "Мираском" Специалист отдела ИТ Высшее образование 2007 Московский государств... 19.04.2019 11:39 Имеется собственный автомобиль
  1. Выведите основную информацию о числе непустых значений в столбцах и их типах в таблице.
  1. Обратите внимание на информацию о числе непустых значений.
In [105]:
#ваш код здесь
display(hh.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 44744 entries, 0 to 44743
Data columns (total 12 columns):
 #   Column                           Non-Null Count  Dtype 
---  ------                           --------------  ----- 
 0   Пол, возраст                     44744 non-null  object
 1   ЗП                               44744 non-null  object
 2   Ищет работу на должность:        44744 non-null  object
 3   Город, переезд, командировки     44744 non-null  object
 4   Занятость                        44744 non-null  object
 5   График                           44744 non-null  object
 6   Опыт работы                      44576 non-null  object
 7   Последнее/нынешнее место работы  44743 non-null  object
 8   Последняя/нынешняя должность     44742 non-null  object
 9   Образование и ВУЗ                44744 non-null  object
 10  Обновление резюме                44744 non-null  object
 11  Авто                             44744 non-null  object
dtypes: object(12)
memory usage: 4.1+ MB
None
  1. Выведите основную статистическую информацию о столбцах.
In [107]:
#ваш код здесь
display(hh.describe())
Пол, возраст ЗП Ищет работу на должность: Город, переезд, командировки Занятость График Опыт работы Последнее/нынешнее место работы Последняя/нынешняя должность Образование и ВУЗ Обновление резюме Авто
count 44744 44744 44744 44744 44744 44744 44576 44743 44742 44744 44744 44744
unique 16003 690 14929 10063 38 47 44413 30214 16927 40148 18838 2
top Мужчина , 32 года , родился 17 сентября 1986 50000 руб. Системный администратор Москва , не готов к переезду , не готов к кома... полная занятость полный день Опыт работы 10 лет 8 месяцев Апрель 2018 — по... Индивидуальное предпринимательство / частная п... Системный администратор Высшее образование 1987 Военный инженерный Кра... 07.05.2019 09:50 Не указано
freq 18 4064 3099 1261 30026 22727 3 935 2062 4 25 32268

Преобразование данных¶

  1. Начнем с простого - с признака "Образование и ВУЗ". Его текущий формат это: <Уровень образования год выпуска ВУЗ специальность...>. Например:
  • Высшее образование 2016 Московский авиационный институт (национальный исследовательский университет)...
  • Неоконченное высшее образование 2000 Балтийская государственная академия рыбопромыслового флота…

Нас будет интересовать только уровень образования.

Создайте с помощью функции-преобразования новый признак "Образование", который должен иметь 4 категории: "высшее", "неоконченное высшее", "среднее специальное" и "среднее".

Выполните преобразование, ответьте на контрольные вопросы и удалите признак "Образование и ВУЗ".

Совет: обратите внимание на структуру текста в столбце "Образование и ВУЗ". Гарантируется, что текущий уровень образования соискателя всегда находится в первых 2ух слов и начинается с заглавной буквы. Воспользуйтесь этим.

Совет: проверяйте полученные категории, например, с помощью метода unique()

In [16]:
#ваш код здесь
def edu_lvl(edu_str):
    if 'высшее' in edu_str.lower():
        if 'неоконченное' in edu_str.lower():
            return 'неоконченное высшее'
        else:
            return 'высшее'
    elif 'среднее специальное' in edu_str.lower():
        return 'среднее специальное'
    elif 'среднее образование' in edu_str.lower():
        return 'среднее'
# Применяем написанную функцию на создаваемый столбец
hh['Образование'] = hh['Образование и ВУЗ'].apply(lambda x: edu_lvl(x))
hh.drop('Образование и ВУЗ', axis = 1, inplace = True)
print(hh["Образование"].unique())
['неоконченное высшее' 'высшее' 'среднее специальное' 'среднее']
  1. Теперь нас интересует столбец "Пол, возраст". Сейчас он представлен в формате <Пол , возраст , дата рождения >. Например:
  • Мужчина , 39 лет , родился 27 ноября 1979
  • Женщина , 21 год , родилась 13 января 2000

Как вы понимаете, нам необходимо выделить каждый параметр в отдельный столбец.

Создайте два новых признака "Пол" и "Возраст". При этом важно учесть:

  • Признак пола должен иметь 2 уникальных строковых значения: 'М' - мужчина, 'Ж' - женщина.
  • Признак возраста должен быть представлен целыми числами.

Выполните преобразование, ответьте на контрольные вопросы и удалите признак "Пол, возраст" из таблицы.

Совет: обратите внимание на структуру текста в столбце, в части на то, как разделены параметры пола, возраста и даты рождения между собой - символом ' , '. Гарантируется, что структура одинакова для всех строк в таблице. Вы можете воспользоваться этим.

In [18]:
#ваш код здесь
def gen_age(info):
    parts = info.split(' , ') 
    gender = 'М' if 'Мужчина' in parts[0] else 'Ж' 
    age = int(parts[1].split()[0])  
    return pd.Series([gender, age])  
# Применяем функцию на создаваемые столбцы
hh[['Пол', 'Возраст']] = hh['Пол, возраст'].apply(gen_age)
hh.drop(columns=['Пол, возраст'], axis = 1, inplace=True)
display(hh[['Пол', 'Возраст']])
Пол Возраст
0 М 39
1 М 60
2 Ж 36
3 М 38
4 Ж 26
... ... ...
44739 М 30
44740 М 27
44741 Ж 48
44742 М 24
44743 М 38

44744 rows × 2 columns

  1. Следующим этапом преобразуем признак "Опыт работы". Его текущий формат - это: <Опыт работы: n лет m месяцев, периоды работы в различных компаниях…>.

Из столбца нам необходимо выделить общий опыт работы соискателя в месяцах, новый признак назовем "Опыт работы (месяц)"

Для начала обсудим условия решения задачи:

  • Во-первых, в данном признаке есть пропуски. Условимся, что если мы встречаем пропуск, оставляем его как есть (функция-преобразование возвращает NaN)
  • Во-вторых, в данном признаке есть скрытые пропуски. Для некоторых соискателей в столбце стоит значения "Не указано". Их тоже обозначим как NaN (функция-преобразование возвращает NaN)
  • В-третьих, нас не интересует информация, которая описывается после указания опыта работы (периоды работы в различных компаниях)
  • В-четвертых, у нас есть проблема: опыт работы может быть представлен только в годах или только месяцах. Например, можно встретить следующие варианты:
    • Опыт работы 3 года 2 месяца…
    • Опыт работы 4 года…
    • Опыт работы 11 месяцев…
    • Учитывайте эту особенность в вашем коде

Учитывайте эту особенность в вашем коде

В результате преобразования у вас должен получиться столбец, содержащий информацию о том, сколько месяцев проработал соискатель. Выполните преобразование, ответьте на контрольные вопросы и удалите столбец "Опыт работы" из таблицы.

In [20]:
#ваш код здесь
def get_experience(arg):
    if arg is np.nan or arg == 'Не указано':
        return None
    year_words=['год', 'года', 'лет']
    month_words=['месяц', 'месяца', 'месяцев']
    arg_splitted = arg.split(' ')[:7]
    years = 0
    months = 0
    for index, item in enumerate (arg_splitted):
        if item in year_words:
            years = int(arg_splitted[index-1])
        if item in month_words:
            months = int(arg_splitted[index-1])
    return int(years*12 + months)
hh['Опыт работы (месяц)'] = hh['Опыт работы'].apply(get_experience)
display(hh['Опыт работы (месяц)'])
0        202.0
1        233.0
2        123.0
3        225.0
4         67.0
         ...  
44739     91.0
44740     84.0
44741    257.0
44742     46.0
44743    190.0
Name: Опыт работы (месяц), Length: 44744, dtype: float64
  1. Хорошо идем! Следующий на очереди признак "Город, переезд, командировки". Информация в нем представлена в следующем виде: <Город , (метро) , готовность к переезду (города для переезда) , готовность к командировкам>. В скобках указаны необязательные параметры строки. Например, можно встретить следующие варианты:
  • Москва , не готов к переезду , готов к командировкам
  • Москва , м. Беломорская , не готов к переезду, не готов к командировкам
  • Воронеж , готов к переезду (Сочи, Москва, Санкт-Петербург) , готов к командировкам

Создадим отдельные признаки "Город", "Готовность к переезду", "Готовность к командировкам". При этом важно учесть:

  • Признак "Город" должен содержать только 4 категории: "Москва", "Санкт-Петербург" и "город-миллионник" (их список ниже), остальные обозначьте как "другие".

    Список городов-миллионников:

    million_cities = ['Новосибирск', 'Екатеринбург','Нижний Новгород','Казань', 'Челябинск','Омск', 'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж','Волгоград']

    Инфорация о метро, рядом с которым проживает соискатель нас не интересует.
  • Признак "Готовность к переезду" должен иметь два возможных варианта: True или False. Обратите внимание, что возможны несколько вариантов описания готовности к переезду в признаке "Город, переезд, командировки". Например:

    • … , готов к переезду , …
    • … , не готова к переезду , …
    • … , готова к переезду (Москва, Санкт-Петербург, Ростов-на-Дону)
    • … , хочу переехать (США) , …

    Нас интересует только сам факт возможности или желания переезда.

  • Признак "Готовность к командировкам" должен иметь два возможных варианта: True или False. Обратите внимание, что возможны несколько вариантов описания готовности к командировкам в признаке "Город, переезд, командировки". Например:

    • … , готов к командировкам , …
    • … , готова к редким командировкам , …
    • … , не готов к командировкам , …

    Нас интересует только сам факт готовности к командировке.

    Еще один важный факт: при выгрузки данных у некоторых соискателей "потерялась" информация о готовности к командировкам. Давайте по умолчанию будем считать, что такие соискатели не готовы к командировкам.

Выполните преобразования и удалите столбец "Город, переезд, командировки" из таблицы.

Совет: обратите внимание на то, что структура текста может меняться в зависимости от указания ближайшего метро. Учите это, если будете использовать порядок слов в своей программе.

In [22]:
#ваш код здесь
def get_city(arg):
# Города-миллионники
    million_cities = ['Новосибирск', 'Екатеринбург', 'Нижний Новгород', 'Казань', 'Челябинск', 'Омск', 
                  'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж', 'Волгоград']
    city = arg.split(' , ')[0]
    if (city == 'Москва') or (city == 'Санкт-Петербург'):
        return city
    elif city in million_cities:
        return 'город миллионник'
    else:
        return 'другие'
def get_ready_to_move(arg):
    if ('не готов к переезду' in arg) or ('не готова к переезду' in arg):
        return False
    elif 'хочу' in arg:
        return True
    else:
        return True
def get_ready_for_bisiness_trips(arg):
    if ('командировка' in arg):
        if ('не готов к командировкам' in arg) or('не готова к командировкам' in arg):
            return False
        else: 
            
            return True
    else:
        return False
    
hh['Город'] = hh['Город, переезд, командировки'].apply(get_city)
hh['Готовность к переезду'] = hh['Город, переезд, командировки'].apply(get_ready_to_move)
hh['Готовность к командировкам'] = hh['Город, переезд, командировки'].apply(get_ready_for_bisiness_trips)
hh = hh.drop('Город, переезд, командировки', axis=1)
display(hh[['Город', 'Готовность к переезду', 'Готовность к командировкам']])
Город Готовность к переезду Готовность к командировкам
0 другие False False
1 другие False True
2 другие False False
3 другие False True
4 Москва False True
... ... ... ...
44739 другие True True
44740 другие True True
44741 город миллионник True True
44742 другие False False
44743 Москва False False

44744 rows × 3 columns

  1. Рассмотрим поближе признаки "Занятость" и "График". Сейчас признаки представляют собой набор категорий желаемой занятости (полная занятость, частичная занятость, проектная работа, волонтерство, стажировка) и желаемого графика работы (полный день, сменный график, гибкий график, удаленная работа, вахтовый метод).

На сайте hh.ru соискатель может указывать различные комбинации данных категорий, например:

  • полная занятость, частичная занятость
  • частичная занятость, проектная работа, волонтерство
  • полный день, удаленная работа
  • вахтовый метод, гибкий график, удаленная работа, полная занятость

Такой вариант признаков имеет множество различных комбинаций, а значит множество уникальных значений, что мешает анализу. Нужно это исправить!

Давайте создадим признаки-мигалки для каждой категории: если категория присутствует в списке желаемых соискателем, то в столбце на месте строки рассматриваемого соискателя ставится True, иначе - False.

Такой метод преобразования категориальных признаков называется One Hot Encoding и его схема представлена на рисунке ниже:

No description has been provided for this image Выполните данное преобразование для признаков "Занятость" и "График", ответьте на контрольные вопросы, после чего удалите их из таблицы
In [24]:
#ваш код здесь
employments = ['полная занятость', 'частичная занятость',
              'проектная работа', 'волонтерство', 'стажировка']
charts = ['полный день', 'сменный график', 
         'гибкий график', 'удаленная работа',
         'вахтовый метод']
for employment, chart in zip(employments, charts):
    hh[employment] = hh['Занятость'].apply(lambda x: employment in x)
    hh[chart] = hh['График'].apply(lambda x: chart in x)
hh = hh.drop('Занятость', axis=1)
hh = hh.drop('График', axis=1)
  1. (2 балла) Наконец, мы добрались до самого главного и самого важного - признака заработной платы "ЗП".

В чем наша беда? В том, что помимо желаемой заработной платы соискатель указывает валюту, в которой он бы хотел ее получать, например:

  • 30000 руб.
  • 50000 грн.
  • 550 USD

Нам бы хотелось видеть заработную плату в единой валюте, например, в рублях. Возникает вопрос, а где взять курс валют по отношению к рублю?

На самом деле язык Python имеет в арсенале огромное количество возможностей получения данной информации, от обращения к API Центробанка, до использования специальных библиотек, например pycbrf. Однако, это не тема нашего проекта.

Поэтому мы пойдем в лоб: обратимся к специальным интернет-ресурсам для получения данных о курсе в виде текстовых файлов. Например, MDF.RU, данный ресурс позволяет удобно экспортировать данные о курсах различных валют и акций за указанные периоды в виде csv файлов. Мы уже сделали выгрузку курсов валют, которые встречаются в наших данных за период с 29.12.2017 по 05.12.2019. Скачать ее вы можете на платформе

Создайте новый DataFrame из полученного файла. В полученной таблице нас будут интересовать столбцы:

  • "currency" - наименование валюты в ISO кодировке,
  • "date" - дата,
  • "proportion" - пропорция,
  • "close" - цена закрытия (последний зафиксированный курс валюты на указанный день).

Перед вами таблица соответствия наименований иностранных валют в наших данных и их общепринятых сокращений, которые представлены в нашем файле с курсами валют. Пропорция - это число, за сколько единиц валюты указан курс в таблице с курсами. Например, для казахстанского тенге курс на 20.08.2019 составляет 17.197 руб. за 100 тенге, тогда итоговый курс равен - 17.197 / 100 = 0.17197 руб за 1 тенге. Воспользуйтесь этой информацией в ваших преобразованиях.

No description has been provided for this image

Осталось только понять, откуда брать дату, по которой определяется курс? А вот же она - в признаке "Обновление резюме", в нем содержится дата и время, когда соискатель выложил текущий вариант своего резюме. Нас интересует только дата, по ней бы и будем сопоставлять курсы валют.

Теперь у нас есть вся необходимая информация для того, чтобы создать признак "ЗП (руб)" - заработная плата в рублях.

После ответа на контрольные вопросы удалите исходный столбец заработной платы "ЗП" и все промежуточные столбцы, если вы их создавали.

Итак, давайте обсудим возможный алгоритм преобразования:

  1. Перевести признак "Обновление резюме" из таблицы с резюме в формат datetime и достать из него дату. В тот же формат привести признак "date" из таблицы с валютами.
  2. Выделить из столбца "ЗП" сумму желаемой заработной платы и наименование валюты, в которой она исчисляется. Наименование валюты перевести в стандарт ISO согласно с таблицей выше.
  3. Присоединить к таблице с резюме таблицу с курсами по столбцам с датой и названием валюты (подумайте, какой тип объединения надо выбрать, чтобы в таблице с резюме сохранились данные о заработной плате, изначально представленной в рублях). Значение close для рубля заполнить единицей 1 (курс рубля самого к себе)
  4. Умножить сумму желаемой заработной платы на присоединенный курс валюты (close) и разделить на пропорцию (обратите внимание на пропуски после объединения в этих столбцах), результат занести в новый столбец "ЗП (руб)".
In [27]:
#ваш код здесь
cur = pd.read_csv("D:/IDE/SF - Assign/ExchangeRates.csv", sep= ",")
def get_salary_num(arg):
    salary = float(arg.split(' ')[0])
    return salary
def get_salary_currency(arg):
    currency_dict = {
        'USD': 'USD', 'KZT': 'KZT',
        'грн': 'UAH', 'белруб': 'BYN',
        'EUR': 'EUR', 'KGS': 'KGS',
        'сум': 'UZS', 'AZN': 'AZN'
    }
    curr = arg.split(' ')[1].replace('.', '')
    if curr == 'руб':
        return 'RUB'
    else:
        return currency_dict[curr]
cur['date'] = pd.to_datetime(cur['date'], dayfirst=True).dt.date
hh['Обновление резюме'] = pd.to_datetime(hh['Обновление резюме'], dayfirst=True).dt.date
hh['ЗП (tmp)'] = hh['ЗП'].apply(get_salary_num)
hh['Курс (tmp)'] = hh['ЗП'].apply(get_salary_currency)
merged = hh.merge(
    cur, 
    left_on=['Курс (tmp)', 'Обновление резюме'],
    right_on=['currency', 'date',], 
    how='left'
)
merged['close'] = merged['close'].fillna(1)
merged['proportion'] = merged['proportion'].fillna(1)
hh['ЗП (руб)'] = merged['close'] * merged['ЗП (tmp)'] / merged['proportion']
hh = hh.drop(['ЗП', 'ЗП (tmp)', 'Курс (tmp)'], axis=1)
C:\Users\user\AppData\Local\Temp\ipykernel_3692\1009455752.py:18: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
  cur['date'] = pd.to_datetime(cur['date'], dayfirst=True).dt.date

Исследование зависимостей в данных¶

  1. Постройте распределение признака "Возраст". Опишите распределение, отвечая на следующие вопросы: чему равна мода распределения, каковы предельные значения признака, в каком примерном интервале находится возраст большинства соискателей? Есть ли аномалии для признака возраста, какие значения вы бы причислили к их числу?

Совет: постройте гистограмму и коробчатую диаграмму рядом.

In [30]:
# ваш код здесь
fig = px.histogram(hh, x='Возраст', title='Распределение возраста соискателей',  marginal='box')
fig.update_layout(xaxis_title='Возраст', yaxis_title='Частота')
fig.show()

Ваши выводы по графику здесь¶

Распределение графика похоже на логнормальное с ассиметрией и не очень выраженным эксцессом. Самый низкий возраст - 14, самый большой - 77 лет. Матожидание находится на уровне 30 лет. Как видно из графика боксплота, медиана находится на уровне 31 года. Точки справа от коробки говорят о наличии выбросов, а именно - с 50 лет. Распределены выбросы линейно до 73 лет, а затем через какойто промежуток идут 76 и 77 лет. Самый дальний выброс наблюдается на уровне 100 лет. Это значение может быть связано как с ненамеренной ошибкой, так и со сделанной специально.

  1. Постройте распределение признака "Опыт работы (месяц)". Опишите данное распределение, отвечая на следующие вопросы: чему равна мода распределения, каковы предельные значения признака, в каком примерном интервале находится опыт работы большинства соискателей? Есть ли аномалии для признака опыта работы, какие значения вы бы причислили к их числу?

Совет: постройте гистограмму и коробчатую диаграмму рядом.

In [33]:
# ваш код здесь
fig = px.histogram(hh, x='Опыт работы (месяц)', title='Распределение опыта работы',  marginal='box')
fig.update_layout(xaxis_title='Опыт работы', yaxis_title='Частота')
fig.show()

Ваши выводы здесь¶

Распределение графика похоже на логнормальное с ассиметрией и не очень выраженным эксцессом. Мода распределения располагается в районе 80-84 месяцев. Самое левое значение на графике распределения соответсвтует 0-4 месяцам, а самое крайнее правое - около 1200 месяцам или 100 годам. Основные наблюдения попадают же в диапазон от 0-4 месяцев до приблизительно 280-284 месяцев (отбросим хвосты). Как видно из графика боксплота, в данных присутствует немалое количество аномалий. Они идут непрерывно приблизительно до показателя в 510 месяцев, а затем становятся более отдаленными друг от друга. Самый крайний выброс находится на уровне 1188 месяцев. Это может быть связано с намеренной или ненамеренной ошибкой или со тех. сбоем

  1. Постройте распределение признака "ЗП (руб)". Опишите данное распределение, отвечая на следующие вопросы: каковы предельные значения признака, в каком примерном интервале находится заработная плата большинства соискателей? Есть ли аномалии для признака возраста? Обратите внимание на гигантские размеры желаемой заработной платы.

Совет: постройте гистограмму и коробчатую диаграмму рядом.

In [36]:
# ваш код здесь
fig = px.histogram(hh, x='ЗП (руб)', title='Распределение зарплаты',  marginal='box')
fig.update_layout(xaxis_title='Зарплата', yaxis_title='Частота')
fig.show()

Ваши выводы здесь¶

Распределение графика похоже на логнормальное с ассиметрией и выраженным эксцессом. Самые крайние левые значения находятся на уровне - (-2500)- 2490 рублей (следует обратить внимание на отрицательные значения, т.к. это, скорее всего ошибка), самое правое - в районе 25 миллионов рублей. Большинство значений расположены в диапозоне от (-2500)- 2490 рублей до 197.5 - 202.49 тыс. рублей (не беря во внимание правый хвост). Аномалии присутствуют, как можно заметить на графике боксплота. Сначала они идут непрерывно и становятся после 1 миллиона все реже. Самое крайнее - 24.3 млн. рублей. Это может быть связано также с ошибками или с очень хорошим мнением о своей уникальности.

  1. Постройте диаграмму, которая показывает зависимость медианной желаемой заработной платы ("ЗП (руб)") от уровня образования ("Образование"). Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 млн рублей.

Сделайте выводы по представленной диаграмме: для каких уровней образования наблюдаются наибольшие и наименьшие уровни желаемой заработной платы? Как вы считаете, важен ли признак уровня образования при прогнозировании заработной платы?

In [39]:
# ваш код здесь
bar_data = hh[hh['ЗП (руб)']<1e6].groupby('Образование', as_index=False).agg({'ЗП (руб)': 'median'})
fig = px.bar(
    data_frame=bar_data,
    x='Образование',
    y='ЗП (руб)',
    title='Медианная з/п по уровню образования'
    )
fig.show()

Ваши выводы здесь¶

На графике барплотов мы видим, что на наибольшую зарплату претендуют потенциальные сотрудники с высшим образованием. На наименьшую (около 40 тыс. рублей) претендуют потенциальные сотрудники со средним и средним специальным уровнем образования. Безусловно, эти данные смогут помочь для прогнозирования, например, в моделях классификации или кластеризации и других.

  1. Постройте диаграмму, которая показывает распределение желаемой заработной платы ("ЗП (руб)") в зависимости от города ("Город"). Используйте для диаграммы данные о резюме, где желая заработная плата меньше 1 млн рублей.

Сделайте выводы по полученной диаграмме: как соотносятся медианные уровни желаемой заработной платы и их размах в городах? Как вы считаете, важен ли признак города при прогнозировании заработной платы?

In [42]:
# ваш код здесь
box_data = hh[hh['ЗП (руб)']<1e6]
fig = px.box(
    data_frame=box_data,
    x='Город',
    y='ЗП (руб)',
    color='Город',
    title='Распределение з/п по городам'
)
fig.show()

Ваши выводы здесь¶

На графике боксплотов мы видим, что медианная желаемая зарплата зависит от города: сначала - Москва, затем - Санкт-Петербург и так далее. Самый большой размах наблюдается в Москве, при этом и нижнее экстремальное значение находится выше, чем в любом другом городе. В Санкт-Петербурге аналогичная ситуация, что и с медианой, но это менее выражено. Данный признак также может являться болезным при прогнозировании зарплат, ведь, как мы видим, желаемые зарплаты имеют некую тенденцию от города к городу.

  1. Постройте многоуровневую столбчатую диаграмму, которая показывает зависимость медианной заработной платы ("ЗП (руб)") от признаков "Готовность к переезду" и "Готовность к командировкам". Проанализируйте график, сравнив уровень заработной платы в категориях.
In [45]:
# ваш код здесь
bar_data = hh.groupby(
    ['Готовность к командировкам', 'Готовность к переезду'],
    as_index=False
)['ЗП (руб)'].median()
fig = px.bar(
    data_frame=bar_data,
    y='Готовность к переезду',
    x='ЗП (руб)',
    barmode="group",
    color='Готовность к командировкам',
    orientation='h',
    title='Медианная з/п по готовности к командировкам/переезду'
)
fig.show()

Ваши выводы здесь¶

На графике барплотов мы видим некую тенденцию: сосикатели, готовые к командировкам и к переезду расчитывают на наибольшую заработную плату (около 66 тыс. руб.). Следущими идут соискатели, согласные на командировки, но не согласные с переездом (около 60 тыс. руб.). Предпоследними по уровню зарплаты идут согласные к переезду, но не согласные с командировками (50 тыс. рублей). И самый низкий по уровню зарплаты медианный сосискатель, не согласный со всеми этими опциями, претендует на зарплату на уровне 40 тыс. рублей.

  1. Постройте сводную таблицу, иллюстрирующую зависимость медианной желаемой заработной платы от возраста ("Возраст") и образования ("Образование"). На полученной сводной таблице постройте тепловую карту. Проанализируйте тепловую карту, сравнив показатели внутри групп.
In [48]:
# ваш код здесь
pivot = hh.pivot_table(
    index='Образование',
    columns='Возраст',
    values='ЗП (руб)',
    aggfunc='median',
    fill_value=0
)
fig = px.imshow(
    pivot,
    aspect='auto',
    color_continuous_scale='greens',
    title='Медианная з/п по образованию и возрасту'
)
fig.show()

Ваши выводы здесь¶

Из тепловой карты заметно, что соискатели, претендующие на самую высокую зарплату, 120 тыс. рублей, имеют следующие характеристики: 1.Высшее образование и возраст 16 лет (высшее образование в 16 лет?); 2.Среднее специальное и возраст 67 лет. В целом можно заметить тендцению, что, чем выше уровень образования - тем на больщую зарплату расчитывает соискатель.

  1. Постройте диаграмму рассеяния, показывающую зависимость опыта работы ("Опыт работы (месяц)") от возраста ("Возраст"). Опыт работы переведите из месяцев в года, чтобы признаки были в едином масштабе. Постройте на графике дополнительно прямую, проходящую через точки (0, 0) и (100, 100). Данная прямая соответствует значениям, когда опыт работы равен возрасту человека. Точки, лежащие на этой прямой и выше нее - аномалии в наших данных (опыт работы больше либо равен возрасту соискателя)
In [51]:
# ваш код здесь
hh['Опыт работы (годы)'] = hh['Опыт работы (месяц)'] / 12
fig = px.scatter(hh, x='Возраст', y='Опыт работы (годы)', 
                 title="Зависимость опыта работы от возраста",
                 labels={'Опыт работы (годы)': 'Опыт работы (годы)', 'Возраст': 'Возраст'})
fig.add_shape(
    type="line", 
    x0=0, y0=0, 
    x1=100, y1=100,
    line=dict(color="Red", width=2, dash="dash")
)
fig.update_xaxes(range=[0, 100], title="Возраст")
fig.update_yaxes(range=[0, 100], title="Опыт работы (годы)")
fig.show()

Ваши выводы здесь¶

График можно рассмотреть самостоятельно, но стоит обратить внимание на точки, лежащие на либо выше прямой - это аномалии. Точки, лежащие ниже прямой имеют тенденцию к сдвигу вверх, тут все предельно ясно: чем выше возраст - тем выше стаж у сосикателя.

Дополнительные баллы

Для получения 2 дополнительных баллов по разведывательному анализу постройте еще два любых содержательных графика или диаграммы, которые помогут проиллюстрировать влияние признаков/взаимосвязь между признаками/распределения признаков. Приведите выводы по ним. Желательно, чтобы в анализе участвовали признаки, которые мы создавали ранее в разделе "Преобразование данных".

In [54]:
# ваш код здесь

median_salary = hh.groupby(['Пол', 'Образование', 'Город'], as_index=False)['ЗП (руб)'].median()
median_salary.rename(columns={'ЗП (руб)': 'Медианная ЗП'}, inplace=True)
fig = px.sunburst(median_salary, path=['Пол', 'Образование', 'Город'], values='Медианная ЗП',
                  title='Круговая диаграмма по полу, образованию и городу (медианная ЗП)',
                  width=800, height=800)
fig.show()

corr = hh[['ЗП (руб)', 'Возраст', 'Опыт работы (месяц)']].corr()
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt=".2f", cmap='coolwarm', square=True)
plt.title('Коррелограмма')
Out[54]:
Text(0.5, 1.0, 'Коррелограмма')
No description has been provided for this image

Ваши выводы здесь¶

На круговой диаграмме можно увидеть разбивку по полу, уровню образования и горду проживания по медианной зарплате. Это может быть полезно для наглядного представления этих существующих характеристик у сосикателей на сайте; на коррелограмме можно увидеть тесноту связи таких характеристик зарплата, возраст и опыт работы (при конкретных целях можно добавить другие). Наиболее тесная связь наблюдается у категорий 'Возраст' и 'Опыт работы'. Эта связь довольно понятно и не нуждается в интерпретации.

Очистка данных¶

  1. Начнем с дубликатов в наших данных. Найдите полные дубликаты в таблице с резюме и удалите их.
In [58]:
# ваш код здесь
dupl = hh[hh.duplicated(subset=hh.columns)]
data = hh.drop_duplicates()
display(dupl.shape[0])
155
  1. Займемся пропусками. Выведите информацию о числе пропусков в столбцах.
In [60]:
# ваш код здесь
null_data = hh.isnull().sum()
display(null_data[null_data > 0])
Опыт работы                        168
Последнее/нынешнее место работы      1
Последняя/нынешняя должность         2
Опыт работы (месяц)                170
Опыт работы (годы)                 170
dtype: int64
  1. Итак, у нас есть пропуски в 3ех столбцах: "Опыт работы (месяц)", "Последнее/нынешнее место работы", "Последняя/нынешняя должность". Поступим следующим образом: удалите строки, где есть пропуск в столбцах с местом работы и должностью. Пропуски в столбце с опытом работы заполните медианным значением.
In [62]:
# ваш код здесь
hh = hh.dropna(subset=['Последнее/нынешнее место работы', 'Последняя/нынешняя должность'])
hh['Опыт работы (месяц)'] = hh['Опыт работы (месяц)'].fillna(hh['Опыт работы (месяц)'].median())
  1. Мы добрались до ликвидации выбросов. Сначала очистим данные вручную. Удалите резюме, в которых указана заработная плата либо выше 1 млн. рублей, либо ниже 1 тыс. рублей.
In [64]:
# ваш код здесь
outliers = hh[(hh['ЗП (руб)'] > 1e6) | (hh['ЗП (руб)'] < 1e3)]
hh = hh.drop(outliers.index)
  1. В процессе разведывательного анализа мы обнаружили резюме, в которых опыт работы в годах превышал возраст соискателя. Найдите такие резюме и удалите их из данных
In [66]:
# ваш код здесь
outliers = hh[hh['Опыт работы (месяц)']/12 >= hh['Возраст']]
hh = hh.drop(outliers.index)
  1. В результате анализа мы обнаружили потенциальные выбросы в признаке "Возраст". Это оказались резюме людей чересчур преклонного возраста для поиска работы. Попробуйте построить распределение признака в логарифмическом масштабе. Добавьте к графику линии, отображающие среднее и границы интервала метода трех сигм. Напомним, сделать это можно с помощью метода axvline. Например, для построение линии среднего будет иметь вид:

histplot.axvline(log_age.mean(), color='k', lw=2)

В какую сторону асимметрично логарифмическое распределение? Напишите об этом в комментарии к графику. Найдите выбросы с помощью метода z-отклонения и удалите их из данных, используйте логарифмический масштаб. Давайте сделаем послабление на 1 сигму (возьмите 4 сигмы) в правую сторону.

Выведите таблицу с полученными выбросами и оцените, с каким возрастом соискатели попадают под категорию выбросов?

In [68]:
# ваш код здесь
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
log_age = np.log(hh['Возраст'] + 1)
histplot = sns.histplot(log_age, bins=30, ax=ax)
histplot.axvline(log_age.mean(), color='k', lw=2)
histplot.axvline(log_age.mean()+ 4 *log_age.std(), color='k', ls='--', lw=2)
histplot.axvline(log_age.mean()- 3 *log_age.std(), color='k', ls='--', lw=2)
histplot.set_title('Log Age Distribution');

def outliers_z_score_mod(hh, feature, left=3, right=3, log_scale=False):
    if log_scale:
        x = np.log(hh[feature]+1)
    else:
        x = hh[feature]
    mu = x.mean()
    sigma = x.std()
    lower_bound = mu - left * sigma
    upper_bound = mu + right * sigma
    outliers = hh[(x < lower_bound) | (x > upper_bound)]
    cleaned = hh[(x >= lower_bound) & (x <= upper_bound)]
    return outliers, cleaned
outliers, cleaned_data = outliers_z_score_mod(hh, 'Возраст', left=3,  right=4, log_scale=True)
display(outliers)
display(list(outliers["Возраст"]))
Ищет работу на должность: Опыт работы Последнее/нынешнее место работы Последняя/нынешняя должность Обновление резюме Авто Образование Пол Возраст Опыт работы (месяц) ... частичная занятость сменный график проектная работа гибкий график волонтерство удаленная работа стажировка вахтовый метод ЗП (руб) Опыт работы (годы)
31137 Менеджер по работе с клиентами Опыт работы 2 месяца Июнь 2018 — Июль 2018 2... ООО "ФёрстКэшКомпани" Менеджер по работе с клиентами 2019-04-06 Не указано среднее М 15 2.0 ... True True False True False True False False 10000.0 0.166667
32950 Тестировщик игр Опыт работы 3 месяца Март 2019 — по настоящее... OOO ЖМЫХ Тестировщик ПО 2019-04-09 Не указано среднее специальное М 15 3.0 ... False False False False False False False False 2000.0 0.250000
33654 Frontend-разработчик Опыт работы 2 года 6 месяцев Февраль 2019 — п... Freelance Frontend-разработчик 2019-04-19 Не указано среднее специальное М 100 30.0 ... True False True True False True True False 60000.0 2.500000

3 rows × 25 columns

[15, 15, 100]
No description has been provided for this image

Ваш коммментарий здесь¶

Ассиметрия (скошенность) на графике распределения наблюдается в левую сторону, т.к. правая, вполне, напоминает нормальное. Как можно увидеть в списке возрастов из таблицы выбросов, то под выбросы попадают возраст 15 и 100 лет.